<template>
  <form
    class="w-form"
    @submit="onSubmit"
    @reset="reset"
    novalidate="novalidate"
    :class="classes"
  >
    <slot></slot>
  </form>
</template>

<script>
import { ref, reactive, computed, watch, provide, onMounted } from "vue";
export default {
  name: "w-form",

  provide() {
    return {
      formRegister: this.register,
      formUnregister: this.unregister,
      validateElement: this.validateElement,
      noKeyupValidation: this.noKeyupValidation,
      noBlurValidation: this.noBlurValidation
    };
  },

  props: {
    modelValue: {},
    allowSubmit: { type: Boolean },
    noKeyupValidation: { type: Boolean },
    noBlurValidation: { type: Boolean },
    errorPlaceholders: { type: Boolean }
  },

  emits: [
    "submit",
    "before-validate",
    "validate",
    "success",
    "error",
    "reset",
    "input",
    "update:modelValue",
    "update:errorsCount"
  ],

  setup(props, ctx) {
    const formElements = ref([]);
    const status = ref(null); // null = pristine, false = error, true = success.
    const errorsCount = ref(0);

    const classes = computed(() => {
      return {
        "w-form--pristine": status.value === null,
        "w-form--error": status.value === false,
        "w-form--success": status.value === true,
        "w-form--error-placeholders": props.errorPlaceholders
      };
    });

    function register(formElement) {
      formElements.value.push(formElement);
    }

    function unregister(formElement) {
      formElements.value = formElements.value.filter(
        item => item._.uid !== formElement._.uid
      );
    }

    /**
     * On form submit or programmatic call, go through each validation rule of each form element,
     * and count the number of errors.
     * Display the element error inside the element if any error, and $emit the result.
     *
     * @param {Object} e the submit event
     * @return {Boolean} true if the form is valid
     */
    function validate(e) {
      ctx.emit("before-validate");
      const errorsCount = formElements.value.reduce((total, el) => {
        const {
          validators,
          Validation = {},
          inputValue,
          readonly,
          disabled
        } = el;

        // Skip validation and return ok if there is no validation or if disabled or readonly.
        if (!validators || disabled || readonly) return total;

        // Execute the validators 1 by 1 until a failure is found. If it happens, raise the error
        // message in the form element.
        validators.some(validator => {
          const result =
            typeof validator === "function" && validator(inputValue);

          Validation.isValid = typeof result !== "string"; // If string, it means there was an error.
          Validation.message = Validation.isValid ? "" : result;
          return !Validation.isValid;
        });

        el.hasJustReset = false;
        // Update the form element's validity. Internal emit not listed in `emits`.
        el.$emit("update:valid", Validation.isValid);
        return total + ~~!Validation.isValid;
      }, 0);

      updateErrorsCount(errorsCount);

      status.value = !errorsCount; // True if valid.

      ctx.emit("validate", { e, errorsCount });
      ctx.emit(status.value ? "success" : "error", { e, errorsCount });
      return status.value;
    }

    function validateElement(el) {
      // Execute the validators 1 by 1 until a failure is found. If it happens, raise the error
      // message in the form element.
      el.validators.some(validator => {
        const result =
          typeof validator === "function" && validator(el.inputValue);

        el.Validation.isValid = typeof result !== "string"; // If string, it means there was an error.
        el.Validation.message = el.Validation.isValid ? "" : result;
        return !el.Validation.isValid;
      });

      el.hasJustReset = false;
      updateErrorsCount();

      return el.Validation.isValid;
    }

    function reset(e) {
      // Reset is called from:
      //   - the form `reset` event listener
      //   - the modelValue watcher when set to `null`.
      // Prevent resetting twice on form reset that sets the modelValue to null.
      if (!e) return;
      status.value = null;

      formElements.value.forEach(
        item => !item.disabled && !item.readonly && item.reset()
      );
      updateErrorsCount(0, true);
      ctx.emit("reset", e);
    }

    function updateErrorsCount(count = null, reset = false) {
      errorsCount.value =
        count !== null
          ? count
          : formElements.value.reduce(
              (sum, el) => sum + ~~(el.Validation.isValid === false),
              0
            );
      status.value = reset ? null : !errorsCount.value;

      ctx.emit("update:modelValue", status.value);
      ctx.emit("input", status.value);
      ctx.emit("update:errorsCount", errorsCount.value);
    }

    function onSubmit(e) {
      ctx.emit("submit", e);
      validate(e);
      if (!props.allowSubmit || !status.value) e.preventDefault();
    }

    onMounted(() => (status.value = props.modelValue || null));

    watch(
      () => props.modelValue,
      value => {
        if (
          (status.value === false && value) ||
          (value === null && status.value !== null)
        )
          reset();
        status.value = value;
      }
    );
    return {
      formElements,
      status,
      errorsCount,
      classes,
      // methods
      register,
      unregister,
      validate,
      validateElement,
      reset,
      updateErrorsCount,
      onSubmit
    };
  }
};
</script>
